// Copyright 2024 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package botapi

import (
	"context"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"go.chromium.org/luci/common/logging"

	"go.chromium.org/luci/swarming/server/botinfo"
	"go.chromium.org/luci/swarming/server/botsrv"
	"go.chromium.org/luci/swarming/server/botstate"
	"go.chromium.org/luci/swarming/server/model"
	"go.chromium.org/luci/swarming/server/validate"
)

// EventRequest is sent by the bot.
type EventRequest struct {
	// Session is a serialized Swarming Bot Session proto.
	Session []byte `json:"session"`

	// RequestUUID is used to skip reporting duplicate events on retries.
	//
	// Generated by the client (usually an UUID4 string). Optional.
	RequestUUID string `json:"request_uuid,omitempty"`

	// Event is the kind of the event the bot is reporting.
	//
	// Required. Must be in the list of allowed events.
	Event model.BotEventType `json:"event"`

	// Message is an optional arbitrary text message associated with the event.
	//
	// Will show up in the UI and when listing bot events in the API. Not
	// interpreted by the server in any way.
	Message string `json:"message,omitempty"`

	// State is (mostly) arbitrary JSON dict with various properties of the bot.
	//
	// Optional. If set, will be used to see if the bot should be quarantined or
	// put into maintenance. If not set, the current bot state in the datastore
	// won't be affected.
	State botstate.Dict `json:"state,omitempty"`

	// Version is the bot's own version, if known.
	//
	// Optional. If set, ends up reported together with the event.
	Version string `json:"version,omitempty"`
}

func (r *EventRequest) ExtractSession() []byte { return r.Session }
func (r *EventRequest) ExtractDebugRequest() any {
	return &EventRequest{
		RequestUUID: r.RequestUUID,
		Event:       r.Event,
		Message:     r.Message,
		State:       r.State,
		Version:     r.Version,
	}
}

// EventResponse is returned by the server.
type EventResponse struct {
	// Empty for now.
}

// allowedBotEvents is the set of event kinds that can be reported by the bot
// via /bot/event API.
var allowedBotEvents = map[model.BotEventType]struct{}{
	model.BotEventError:     {},
	model.BotEventLog:       {},
	model.BotEventRebooting: {},
	model.BotEventShutdown:  {},
}

// Event implements the handler that logs events sent by the bot.
func (srv *BotAPIServer) Event(ctx context.Context, body *EventRequest, r *botsrv.Request) (botsrv.Response, error) {
	if body.Event == "" {
		return nil, status.Errorf(codes.InvalidArgument, "event type is required")
	}
	if _, ok := allowedBotEvents[body.Event]; !ok {
		return nil, status.Errorf(codes.InvalidArgument, "unsupported event type %q", body.Event)
	}
	if err := validate.BotRequestUUID(body.RequestUUID); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "bad request_uuid %q: %s", body.RequestUUID, err)
	}
	if err := body.State.Err(); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "bad state JSON dict: %s", err)
	}

	// If the request has the state populated, derive the health info based on it.
	// Otherwise do not change the state or the health status of the bot.
	var botState *botstate.Dict
	var botHealth *botinfo.HealthInfo
	if !body.State.IsEmpty() {
		health, state, err := updateBotHealthInfo(body.State, r.Dimensions.DimensionValues(botstate.QuarantinedKey), nil)
		if err != nil {
			return nil, status.Errorf(codes.Internal, "failed to update the bot state dict: %s", err)
		}
		botState = &state
		botHealth = &health
	}

	update := &botinfo.Update{
		BotID:         r.Session.BotId,
		State:         botState,
		EventType:     body.Event,
		EventDedupKey: body.RequestUUID,
		EventMessage:  body.Message,
		TasksManager:  srv.tasksManager,
		CallInfo: botCallInfo(ctx, &botinfo.CallInfo{
			SessionID: r.Session.SessionId,
			Version:   body.Version,
		}),
		HealthInfo: botHealth,
	}
	if err := srv.submitUpdate(ctx, update); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to update bot info: %s", err)
	}

	// TODO(b/355012930): Report to Cloud Error Reporting instead. Looks like we
	// may need a custom client, since cloud.google.com/go/errorreporting assumes
	// all errors are originating in the running process. But we are just relaying
	// existing errors that happened elsewhere. Using a custom client would allow
	// us to report the version, bot ID, the bot code stack trace, etc. This may
	// be useful for more intelligent grouping in the Cloud Error Reporting UI.
	if body.Event == model.BotEventError {
		msg := body.Message
		if msg == "" {
			msg = "<no error message provided>"
		}
		logging.Errorf(ctx, "Bot error from %s: %s", r.Session.BotId, msg)
	}

	return &EventResponse{}, nil
}
